--------------------------------------------------------------------------
-- $File: //scripts/astrada/ServerModUtils.lua $
-- $Revision$
-- $Date$
-- $Author$
--
-- $Id$
--------------------------------------------------------------------------
-- This file is maintained as part of the SSM AEGIS project. All additions
-- are the sole work of their authors to which all copyright and ownership
-- resides. However, each author grants license for the use of his or her
-- work to be a part of the SSM AEGIS project.
--
--  (C), Daniel 'Rod-Serling' Phelps, 2008
--  (C), Terry 'Charlie' Reinert, 2008
--  Edited/Expanded By:
--  (C), Richard 'DarkLite' May, 2009
--  (C), Bernd 'TITAN', 2009
--------------------------------------------------------------------------
--  This work is based off of:
--	(C), Ralf 'Hawkeye' Ullrich, 2008
--	ServerModUtils.lua,v 1.1.2.1 2008/07/29 10:41:38
--  See crysis.jnana.de for full original versions.
--------------------------------------------------------------------------

-- ===============================================
-- Server Side Modification Utilities
-- ===============================================
-- ===============================================

function XRemoveTag(player, tag)
	return string.sub(player:GetName(),string.len(tag)+1, string.len(player:GetName()))
end

function XSendBigBlinkMessage(player, msg, time, period, color1, color2)
	local timeDone = 0
	local color = 1
	while timeDone<time do
	    timeDone = timeDone + period
	    if (color==1) then
        	Script.SetTimer(timeDone, function() XSendBigCenterMessageToPlayer(player, "<font color=\""..color1.."\">"..msg.."</font>") end)
		else
		    Script.SetTimer(timeDone, function() XSendBigCenterMessageToPlayer(player, "<font color=\""..color2.."\">"..msg.."</font>") end)
		end
	end
end

function XGetChatCommandFiles()
    LogAlways("Getting Chat Commands:");
    local directory = XRuntimeVar.serverConfigDir.."Chat_commands";
    directory = string.gsub(directory, "/", [[\]]);
    os.execute('cd '..directory..' && dir /a:-d /s /b > "TempChatCommFile.txt"');
    --os.execute('dir '..directory..' /a:-d /s /b > "TempChatCommFile.txt"');
    --os.execute('dir /a:-d /s /b > "TempChatCommFile.txt"');
    local ChatFile, Err = io.open(XRuntimeVar.serverConfigDir.."Chat_commands/TempChatCommFile.txt","r");
    if (not ChatFile) then
        LogAlways("Error loading Chat Commands.");
        return;
    end

    for line in (ChatFile:lines()) do
        LogAlways("Loading "..tostring(line));
        line = tostring(line);
        NewDir = string.gsub(line, [[\]], "/");
        if (string.lower(string.sub(NewDir, -3))=="lua")then
            Script.ReloadScript(NewDir);
        end
    end -- for loop

    ChatFile:close()
    os.execute('cd '..directory..' && del "TempChatCommFile.txt"');
end



function XGetModFiles(type)
    LogAlways("Getting Mods");
    if(XRuntimeVar.serverConfigDir and XRuntimeVar.serverConfigDir~="") then
    	local directory = XRuntimeVar.serverConfigDir.."Modules";
    	directory = string.gsub(directory, "/", [[\]]);
    	if(type) then
        	directory = directory..[[\]]..type;
    	end
    	os.execute('cd '..directory..' && dir /a:-d /b > "TempModFile.txt"');
    
    	local ModFile, Err = io.open(XRuntimeVar.serverConfigDir.."Modules/"..type.."/TempModFile.txt","r");
    	if (not ModFile) then
        	LogAlways("Error loading Mods.");
        	return;
    	end

    	for line in (ModFile:lines()) do
        	local file = XFormat("%s/%s", directory, tostring(line))
        	LogAlways("Loading "..file);
        	--line = tostring(line);
        	NewDir = string.gsub(file, [[\]], "/");
        	if (string.lower(string.sub(NewDir, -3))=="lua")then
           		Script.ReloadScript(NewDir);
        	end
    	end -- for loop

    	ModFile:close()
    	os.execute('cd '..directory..' && del "TempModFile.txt"');
    end
end


function XIsVehicleNearby(pos)
	local ents = System.GetEntitiesInSphere(pos,20);
	for i,e in ipairs(ents) do
		if (e.vehicle) then
			return true;
		end
	end
	return false;
end



-- string.format with full traceback as error message:
function XFormat(fmt,...)
	local status, result = pcall(string.format,fmt,...);
	if (not status) then
		System.LogAlways("[AEGIS] XFormat Error: "..(debug.traceback(tostring(result) or "NIL",2) or "FAILED TRACEBACK"));
		error(tostring(result) or "UNKNOWN ERROR");
	end
	return result;
end

-- formatted log to console log in any verbositiy level
function LogAlways(fmt, ...)
	System.LogAlways(XFormat(fmt, ...));
end
function LogAlwaysError(fmt, ...)
	System.LogAlways("$4"..XFormat(fmt, ...).."  ");
end
function LogAlwaysWarning(fmt, ...)
	System.LogAlways("$6"..XFormat(fmt, ...).."  ");
end


-- formatted log to console log in any verbositiy level
function LogInfo(fmt, ...)
	if (not XCfgVar or XCfgVar.logLevelInfoEnabled) then
		System.LogAlways(XFormat("$8"..fmt, ...).."  ");
	end
end

-- set log verbosity, returns previous level
function LogVerbosity(level)
	local old = System.GetCVar("log_verbosity");
	if (level) then
		System.SetCVar("log_verbosity",level);
	end
	return old;
end

-- formatted log to console of all online admins
function LogAdmins(fmt, ...)
	local msg = XFormat(fmt, ...);
	System.LogAlways(msg);
	if (g_gameRules) then
		local players=g_gameRules.game:GetPlayers();
		if (players) then
			for i,player in pairs(players) do
				if (XIsAdmin(player)) then
					if (not player.x_msg_queue_console) then
						player.x_msg_queue_console = {};
					end
					table.insert(player.x_msg_queue_console,msg);
				end
			end
		end
	end
end

-- formatted log to console of all online admins (without log to server console)
function SendAdmins(fmt, ...)
	local msg = XFormat(fmt, ...);
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		for i,player in pairs(players) do
			if (XIsAdmin(player)) then
				if (not player.x_msg_queue_console) then
					player.x_msg_queue_console = {};
				end
				-- g_gameRules.game:SendTextMessage(TextMessageConsole, msg, TextMessageToClient, player.id);
				table.insert(player.x_msg_queue_console,msg);
			end
		end
	end
end

-- send hit infos
function SendHit(shooter, target, msgAdmin, msgNormal, isOK, isKillShot)
	if (isOK and not isKillShot and not XCfgVar.sendHitReportsToPlayers and not XCfgVar.sendHitDetailsToAdmins) then
		-- dont waste time looping
		return;
	end
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		for i,player in pairs(players) do
			if (XIsAdmin(player)) then
				if ((not isOK or isKillShot or XCfgVar.sendHitDetailsToAdmins) or (XCfgVar.sendHitReportsToPlayers and ((player==shooter) or (player==target)))) then
					if (not player.x_msg_queue_console) then
						player.x_msg_queue_console = {};
					end
					-- g_gameRules.game:SendTextMessage(TextMessageConsole, msg, TextMessageToClient, player.id);
					table.insert(player.x_msg_queue_console,msgAdmin);
				end
			elseif (XCfgVar.sendHitReportsToPlayers and ((player==shooter) or (player==target))) then
				table.insert(player.x_msg_queue_console,msgNormal);
			end
		end
	end
end

--
function LogHacker(fmt,...)
	local msg = XFormat(fmt, ...);
	LogAdmins("[AEGIS] HACK: %s",msg);
	LogHit("[AEGIS] HACK: %s",msg);
end

function XLogHackerDetails(player, hack)
	if (player) then
		XHackerLogWrite(XFormat("Hacker Information:  Name: %s |-| Hack: %s |-| Profile: %s |-| IP: %s |-| Port: %s |-| Host: %s |-| GUID: %s |-| \n", player.info.name, hack, player.info.profile, player.info.ip, player.info.port, player.info.host, player.info.guid));
	end
end

function XHackerLogWrite(line)
	local file,err;
	file = XVar.hackerLogFile;
	if (not file) then
		if (not XVar.hackerLogFileName) then
			local dir = XRuntimeVar.serverLogDir;
			if (not dir or (dir=="")) then
				dir = XRuntimeVar.serverRootDir;
			end
			if (not dir) then
				dir = "";
			end
			XVar.hackerLogFileName = dir.."AEGIS-HACKER-"..XTimeStamp()..".log";
		end
		file,err = io.open(XVar.hackerLogFileName,"w+");
		if (file) then
			XVar.hackerLogFile = file;
			LogAlways("[AEGIS] opened hacker record for writing: %s",XVar.hackerLogFileName);
			XVar.hackerLogFileName = nil;
		end
	end
	if (file) then
		file:write(line);
		file:flush();
	else
		LogAlways("%s",line);
	end
end

-- send a server message to a specific player client
function XSendServerMessageToPlayer(player, fmt, ...)
	g_gameRules.game:SendTextMessage(TextMessageServer, XFormat(fmt,...), TextMessageToClient, player.id);
end

-- send a center message to a specific player client
function XSendCenterMessageToPlayer(player, fmt, ...)
	g_gameRules.game:SendTextMessage(TextMessageCenter, XFormat(fmt,...), TextMessageToClient, player.id);
end

-- send a server message to a all players
function XSendServerMessageToAllRemote(fmt, ...)
	g_gameRules.game:SendTextMessage(TextMessageServer, XFormat(fmt,...), TextMessageToAllRemote);
end

-- send a center message to a specific player client
function XSendCenterMessageToAllRemote(fmt, ...)
	g_gameRules.game:SendTextMessage(TextMessageCenter, XFormat(fmt,...), TextMessageToAllRemote);
end


-- send a console message to a specific player client
function XSendConsoleMessageToPlayer(player, fmt, ...)
	if (not player.x_msg_queue_console) then
		player.x_msg_queue_console = {};
	end
	-- g_gameRules.game:SendTextMessage(TextMessageConsole, XFormat(fmt,...), TextMessageToClient, player.id);
	table.insert(player.x_msg_queue_console, XFormat(fmt,...));
end

-- send a console message to all players of a team
function XSendConsoleMessageToTeam(teamId, fmt, ...)
	local msg = XFormat(fmt, ...);
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		for i,player in pairs(players) do
			local playerTeamId = g_gameRules.game:GetTeam(player.id);
			if (playerTeamId==teamId) then
				if (not player.x_msg_queue_console) then
					player.x_msg_queue_console = {};
				end
				table.insert(player.x_msg_queue_console,msg);
			end
		end
	end
end

-- send a console message to all players of a team and admins
function XSendConsoleMessageToTeamAndAdmins(teamId, fmt, ...)
	local msg = XFormat(fmt, ...);
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		for i,player in pairs(players) do
			local playerTeamId = g_gameRules.game:GetTeam(player.id);
			if (playerTeamId==teamId) or (XIsAdmin(player)) then
				if (not player.x_msg_queue_console) then
					player.x_msg_queue_console = {};
				end
				table.insert(player.x_msg_queue_console,msg);
			end
		end
	end
end

-- send a console message to all players
function XSendConsoleMessageToAll(fmt, ...)
	local msg = XFormat(fmt, ...);
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		for i,player in pairs(players) do
			if (not player.x_msg_queue_console) then
				player.x_msg_queue_console = {};
			end
			table.insert(player.x_msg_queue_console,msg);
		end
	end
end

-- send a big center message to all players
function XSendBigCenterMessage(...)
	local msg = table.concat({...}," ");
	g_gameRules.game:SendTextMessage(TextMessageBig, XFormat("%s",msg), TextMessageToAllRemote);
end

-- send a big center message to one player
function XSendBigCenterMessageToPlayer(player, ...)
	local msg = table.concat({...}, " ");
	g_gameRules.game:SendTextMessage(TextMessageBig, XFormat("%s",msg), TextMessageToClient, player.id);
end

-- send an info message to all players
function XSendInfoMessage(...)
	local msg = table.concat({...}," ");
	g_gameRules.game:SendTextMessage(TextMessageInfo, XFormat("%s",msg), TextMessageToAllRemote);
end

-- send an info message to one player
function XSendInfoMessageToPlayer(player,...)
	local msg = table.concat({...}," ");
	g_gameRules.game:SendTextMessage(TextMessageInfo, XFormat("%s",msg), TextMessageToClient, player.id);
end



-- send a big message to team
function XSendBigMsgToTeam(teamId, fmt, ...)
	local msg = XFormat(fmt, ...);
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		for i,player in pairs(players) do
			local playerTeamId = g_gameRules.game:GetTeam(player.id);
			if (playerTeamId==teamId) then
				g_gameRules.game:SendTextMessage(TextMessageBig, XFormat("%s%s%s",XCfgVar.colorBigCenterMessages,msg,XCfgVar.colorBigCenterMessagesClose), TextMessageToClient, player.id);
			end
		end
	end
end


-- ===============================================
-- Send Chat Messages
-- ===============================================

function XSendChatToAdmins(fmt, ...)
	local msg = XFormat(fmt, ...);
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		for i,player in pairs(players) do
			if (XIsAdmin(player)) then
				XSendChatGeneric(ChatToTarget, nil, player, msg);
			end
		end
	end
end

function XSendChatToTarget(from, to, fmt, ...)
	XSendChatGeneric(ChatToTarget, from, to, XFormat(fmt, ...));
end

function XSendChatToTeam(from, fmt, ...)
	XSendChatGeneric(ChatToTeam, from, from, XFormat(fmt, ...));
end

function XSendChatToAll(from, fmt, ...)
	XSendChatGeneric(ChatToAll, from, from, XFormat(fmt, ...));
end

function XSendChatGeneric(mode, from, to, msg)
	local fromId = NULL_ENTITY;
	local toId = NULL_ENTITY;
	if (from) then
		fromId = from.id;
	else --If no "from" has been passed, check for SSM Chat Entity and use that.
		if (System.GetEntityByName(XCfgVar.ChatEntityName)) then
			fromId = System.GetEntityByName(XCfgVar.ChatEntityName).id
		else --If ChatEntity not found, resort to NULL_ENTITY, and log an error.
			LogAdmins("[AEGIS] $4WARNING! CHAT ENTITY NOT FOUND!")
			LogAdmins("[AEGIS] $4Attempting to reload Chat Entity")
			SetSSMChatEntity();
			if (System.GetEntityByName(XCfgVar.ChatEntityName)) then
				fromId = System.GetEntityByName(XCfgVar.ChatEntityName).id
			else
				LogAdmins("[AEGIS] $4CHAT ENTITY RELOAD FAILED!")
				LogAdmins("[AEGIS] $4CHAT NOT PROPERLY CONFIGURED!")
			end
		end  --Continue anyway with NULL_ENTITY as the fromId
	end
	if (to) then
		toId = to.id;
	else
		if (System.GetEntityByName(XCfgVar.ChatEntityName)) then
			toId = System.GetEntityByName(XCfgVar.ChatEntityName).id
		else --If ChatEntity not found, resort to NULL_ENTITY, and log an error.
			LogAdmins("[AEGIS] $4WARNING! CHAT ENTITY NOT FOUND!")
			LogAdmins("[AEGIS] $6Attempting to reload Chat Entity")
            SetSSMChatEntity();
			if (System.GetEntityByName(XCfgVar.ChatEntityName)) then
				fromId = System.GetEntityByName(XCfgVar.ChatEntityName).id
			else
				LogAdmins("[AEGIS] $4CHAT ENTITY RELOAD FAILED!")
				LogAdmins("[AEGIS] $4CHAT NOT PROPERLY CONFIGURED!")
			end
		end  --Continue anyway with NULL_ENTITY as the toId
	end
	if (fromId==System.GetEntityByName(XCfgVar.ChatEntityName).id) then
		if(mode==ChatToAll) then
			XSendConsoleMessageToAll("$9[CHT] $8%s$9 to $1ALL$9: $5%s",XCfgVar.ChatEntityName,msg);
		elseif(mode==ChatToTeam) then
		 	XSendConsoleMessageToTeamAndAdmins(teamId,"$9[CHT] $8%s$9 to $1TEAM$9: $5%s",XCfgVar.ChatEntityName,msg);
		elseif(mode==ChatToTarget) then
			SendAdmins("$9[CHT] $8%s$9 to $8%s$9: $5%s",XCfgVar.ChatEntityName,to:GetName(),msg);
			if(not XIsAdmin(to)) then --Admins already got it, so don't send it again to them.
				XSendConsoleMessageToPlayer(to, "$9[CHT] $8%s$9 to $8%s$9: $5%s",XCfgVar.ChatEntityName,to:GetName(),msg);
			end
		end
		g_gameRules.game:SendChatMessage(mode, fromId, toId, msg);
	end
end

-- ===============================================
-- Admin Identity Handling
-- ===============================================

function XIsAdminId(playerId)
	return XIsAdmin(System.GetEntity(playerId));
end

function XIsAdmin(player)
	if (not player or XGetIdentityValue(player, "buildInfo")==1 or not XCfgVar or not XCfgVar.adminIds) then
		return false;
	end
	if (XCfgVar.adminIds[XGetIdentityValue(player, "profile")]) then
		return true;
	end
	return false;
end

function XIsProtected(player)
	if (not XIsAdmin(player)) then
		return false;
	end
	if (XCfgVar.adminTag) then
		local tag = string.lower(XCfgVar.adminTag);
		local len = string.len(tag);
		local name = string.lower(player:GetName());
		local prefix = string.sub(name,1,len);
		return tag==prefix;
	end
	return false;
end

function XIsImposter(player)
	if (not player or XGetIdentityValue(player, "buildInfo")==1 or not XCfgVar or not XCfgVar.adminIds) then
		return false;
	end
	if (XCfgVar.adminIds[XGetIdentityValue(player, "profile")]) then
		return false;
	end
	if (XCfgVar.adminTag) then
		local tag = "["..XCfgVar.adminTag.."]";
		local len = string.len(tag);
		local name = player:GetName();
		local prefix = string.sub(name,1,len);
		return tag==prefix;
	end
	return false;
end

-- ===============================================
-- Premium Identity Handling
-- ===============================================

function XIsPremiumId(playerId)
	return XIsPremium(System.GetEntity(playerId));
end

function XIsPremium(player)
	if (not player or XGetIdentityValue(player, "buildInfo")==1 or not XCfgVar or not XCfgVar.premiumIds) then
		return false;
	end
	if (XCfgVar.premiumIds[XGetIdentityValue(player, "profile")]) then
		return true;
	end
	return false;
end

-- ===============================================
-- Player Identity Handling
-- ===============================================


--Safe Call to get identity values. Removes the need for "if (player and player.info and player.info.whatever) then" to prevent nil value errors
function XGetIdentityValue(player, value)
	if (player and player.info and player.info[value]) then
		return player.info[value]
	end
	return "0";
end

function XGetPlayerForIdentityValue(identifier, info)
	if (identifier=="slot" or identifier=="guid" or identifier=="ip" or identifier=="name" or identifier=="channel" or identifier=="host" or identifier=="profile") then
		local players=g_gameRules.game:GetPlayers();
		if (players) then
			for i,player in pairs(players) do
				local value = XGetIdentityValue(player, identifier);
				if (value == info) then
					return player;
				end
			return nil;
			end
		end
	else
		return nil;
	end
end

function XGetPlayersForIdentityValue(identifier, info)
	local players=g_gameRules.game:GetPlayers();
	if (players) then
		local matches = {};
		for i,player in pairs(players) do
			local value = XGetIdentityValue(player, identifier);
			if (value == info) then
				table.insert(matches, player.info.profile);
			end
		end
		if (#matches>0) then
			return matches;
		else
			return nil;
		end
	end
end

-- ===============================================
-- Hit log
-- ===============================================

function LogHit(fmt,...)
	XHitLogWrite(XFormat("%s "..fmt.."\n",XTimeStamp(),...));
end

function XHitLogWriteTS(line)
	XHitLogWrite(XTimeStamp().." "..line);
end

function XHitLogWrite(line)
	local file,err;
	local Hfile,err;
	file = XVar.hitLogFile;
	Hfile = XVar.hackLogFile;
	if (not file) then
		if (not XVar.hitLogFileName) then
			local dir = XRuntimeVar.serverLogDir;
			if (not dir or (dir=="")) then
				dir = XRuntimeVar.serverRootDir;
			end
			if (not dir) then
				dir = "";
			end
			XVar.hitLogFileName = dir.."AEGIS-HIT-"..XTimeStamp()..".log";
		end
		file,err = io.open(XVar.hitLogFileName,"w+");
		if (file) then
			XVar.hitLogFile = file;
			XVar.hitLogFileLastFlush = _time;
			LogAlways("[AEGIS] opened hit log for writing: %s",XVar.hitLogFileName);
			XVar.hitLogFileName = nil;
		end
	end
	if (string.match(line, "HACK")) or (string.match(line, "SUSP")) then
		if (not Hfile) then 
			if (not XVar.hackLogFileName) then
				local dir = XRuntimeVar.serverLogDir;
				if (not dir or (dir=="")) then
					dir = XRuntimeVar.serverRootDir;
				end
				if (not dir) then
					dir = "";
				end
				XVar.hackLogFileName = dir.."AEGIS-HACK-"..XTimeStamp()..".log";
			end
			Hfile,err = io.open(XVar.hackLogFileName,"w+");
			if (Hfile) then
				XVar.hackLogFile = Hfile;
				XVar.hackLogFileLastFlush = _time;
				LogAlways("[AEGIS] opened hit log for writing: %s",XVar.hackLogFileName);
				XVar.hackLogFileName = nil;
			end
		end
		if (Hfile) then
			Hfile:write(line);
			if ((_time - XVar.hackLogFileLastFlush)>5) then
				Hfile:flush();
				XVar.hackLogFileLastFlush = _time;
			end
		end
	end
	if (file) then
		file:write(line);
		if ((_time - XVar.hitLogFileLastFlush)>5) then
			file:flush();
			XVar.hitLogFileLastFlush = _time;
		end
	else
		LogAlways("%s",line);
	end
end



-- ===============================================
-- Miscellaneous Functions
-- ===============================================


-- call a function without disruption on possible exceptions
function XSafeCall(...)
	local status, result = pcall(...);
	if (not status) then
		LogAlways("[AEGIS] Error: %s",tostring(result));
		result = nil;
	end
	return result;
end

-- call a function with verbosity on 5
function XDebugCall(...)
	local lvl = LogVerbosity((XCfgVar and XCfgVar.logDebugVerbosity) or 8);
	local status, result = pcall(...);
	if (not status) then
		LogAlways("[AEGIS] Error: %s",tostring(result));
		result = nil;
	end
	LogVerbosity(lvl);
	return result;
end


-- returns a shallow clone of the specified table
function XShallowCloneTable(table)
	local clone = {};
	for i,v in pairs(table) do
		clone[i] = v;
	end
	return clone;
end


-- returns a clone of the specified table
function XCloneTable(table)
	local clone = {};
	for i,v in pairs(table) do
		if((type(v) == "table") and (v ~= table))then  -- avoid recursing into itself
			clone[i] = XCloneTable(v);
		else
			clone[i] = v;
		end
	end
	return clone;
end

-- Merges the second table into the first
if (not X_MERGE_AS_NIL) then
	X_MERGE_AS_NIL = function() end;
end
function XMergeTable(table, partial)
	-- no change if partial is nil
	if (partial == nil) then
		return table;
	end
	-- set to nil if partial is special singleton value
	if (partial == X_MERGE_AS_NIL) then
		return nil;
	end
	-- just return the new value if partial is not a table
	if (type(partial)~="table") then
		return partial;
	end
	-- if the receiver is not a table, replace it with
	-- a clone of partial
	if (type(table)~="table") then
		return XCloneTable(partial);
	end
	-- now we established table and partial are both tables
	-- however we use two kinds of tables: lists and maps
	-- lists should not be merged but replaced like values
	-- only maps get merged key by key of partial
	--
	-- if either table or partial are lists we just
	-- replace table with a clone of partial
	if ((#table~=0) or (#partial~=0)) then
		return XCloneTable(partial);
	end
	for name,value in pairs(partial) do
		table[name] = XMergeTable(table[name],value);
	end
	-- finally return the modified table
	return table;
end

-- TODO add some comment
function XDumpObject(prefix, obj, depth, short, omitFunctions)
	if (obj and ((obj == XVar.dumpCache) or (obj == XVar.tableXref) or (obj == XVar.functionXref))) then
		return;
	end
	if (depth==nil) then
		depth = 5;
	end
	if (depth<=0) then
		LogAlways("%s=%s ...?", prefix, tostring(obj));
		return
	end
	local clearCache = false;
	if (not XVar.dumpCache) then
		XVar.dumpCache = {};
		XVar.stringMeta = getmetatable("abc");
		XVar.tableXref = {};
		XVar.functionXref = {};
		clearCache = true;
		LogAlways("===== Global Environment Reference");
	end
	local t = type(obj);
	if (t=="table") then
		XDumpTable(prefix..".", obj, depth, short, omitFunctions);
	else
		local s = tostring(obj);
		if ((t=="function") and not omitFunctions) then
			if (not XVar.functionXref[obj]) then
				XVar.functionXref[obj] = {};
			end
			XVar.functionXref[obj][#XVar.functionXref[obj] + 1] = prefix;
			LogAlways("%s=%s", prefix, s);
		elseif (t~="function") then
			LogAlways("%s=%s", prefix, s);
		end
	end
	local m = getmetatable(obj);
	if (m and m~=XVar.stringMeta) then
		XDumpTable(prefix.."::", m, depth, short, omitFunctions);
	end
	if (clearCache) then
		if (not short) then
			LogAlways("===== Table Cross Reference");
			for t,pp in pairs(XVar.tableXref) do
				if (#pp > 1) then
					LogAlways("%s:",t);
					for i,p in ipairs(pp) do
						LogAlways("  %4d=%s",i,p);
					end
				end
			end
			if (not omitFunctions) then
				local fnref = XVar.functionXref;
				XVar.functionXref = {};
				LogAlways("===== Function Cross Reference");
				for fn,pp in pairs(fnref) do
					local info = debug.getinfo(fn,"S");
					LogAlways("%s: %s:%d",tostring(fn),info.source,info.linedefined);
					for i,p in ipairs(pp) do
						LogAlways("    %4d=%s",i,p);
					end
				end
			end
		end
		XVar.functionXref = nil;
		XVar.dumpCache = nil;
		XVar.stringMeta = nil;
		XVar.tableXref = nil;
	end
end

function XDumpTable(prefix, obj, depth, short, omitFunctions)
	local s = tostring(obj);
	if (not XVar.tableXref[s]) then
		XVar.tableXref[s] = {};
	end
	XVar.tableXref[s][#XVar.tableXref[s] + 1] = prefix.."*";
	if (XVar.dumpCache[s]) then
		LogAlways("%s*=%s  <<< see above", prefix, s);
	else
		LogAlways("%s*=%s", prefix, s);
		XVar.dumpCache[s] = true;
		local keys = {};
		for k,v in pairs(obj) do
			keys[#keys + 1] = k;
		end
		table.sort(keys, function (a,b) return XTableComp(a,keys[a],b,keys[b]); end);
		for idx,k in ipairs(keys) do
			local v = obj[k];
			XDumpObject(prefix..tostring(k), v, depth-1, short, omitFunctions);
			if ((k=="id") and (type(v)=="userdata")) then
				local e = System.GetEntity(v);
				if (e and e~=obj) then
					XDumpObject("<"..prefix..".id>", e, depth-1, short, omitFunctions);
				end
			end
		end
	end
end

function XDumpObjectToFile(file, prefix, obj, depth, short, omitFunctions)
	if (obj and ((obj == XVar.dumpCache) or (obj == XVar.tableXref) or (obj == XVar.functionXref))) then
		return;
	end
	if (depth==nil) then
		depth = 5;
	end
	if (depth<=0) then
		file:write(XFormat("%s=%s ...?\n", prefix, tostring(obj)));
		return
	end
	local clearCache = false;
	if (not XVar.dumpCache) then
		XVar.dumpCache = {};
		XVar.stringMeta = getmetatable("abc");
		XVar.tableXref = {};
		XVar.functionXref = {};
		clearCache = true;
		file:write(XFormat("===== Global Environment Reference\n"));
	end
	local t = type(obj);
	if (t=="table") then
		XDumpTableToFile(file,prefix..".", obj, depth, short, omitFunctions);
	else
		local s = tostring(obj);
		if ((t=="function") and not omitFunctions) then
			if (not XVar.functionXref[obj]) then
				XVar.functionXref[obj] = {};
			end
			XVar.functionXref[obj][#XVar.functionXref[obj] + 1] = prefix;
			file:write(XFormat("%s=%s\n", prefix, s));
		elseif (t~="function") then
			file:write(XFormat("%s=%s\n", prefix, s));
		end
	end
	local m = getmetatable(obj);
	if (m and m~=XVar.stringMeta) then
		XDumpTableToFile(file,prefix.."::", m, depth, short, omitFunctions);
	end
	if (clearCache) then
		if (not short) then
			file:write(XFormat("===== Table Cross Reference\n"));
			for t,pp in pairs(XVar.tableXref) do
				if (#pp > 1) then
					file:write(XFormat("%s:\n",t));
					for i,p in ipairs(pp) do
						file:write(XFormat("  %4d=%s\n",i,p));
					end
				end
			end
			if (not omitFunctions) then
				local fnref = XVar.functionXref;
				XVar.functionXref = {};
				file:write(XFormat("===== Function Cross Reference\n"));
				for fn,pp in pairs(fnref) do
					local info = debug.getinfo(fn,"S");
					file:write(XFormat("%s: %s:%d\n",tostring(fn),info.source,info.linedefined));
					for i,p in ipairs(pp) do
						file:write(XFormat("    %4d=%s\n",i,p));
					end
				end
			end
		end
		XVar.functionXref = nil;
		XVar.dumpCache = nil;
		XVar.stringMeta = nil;
		XVar.tableXref = nil;
	end
end

function XDumpTableToFile(file, prefix, obj, depth, short, omitFunctions)
	local s = tostring(obj);
	if (not XVar.tableXref[s]) then
		XVar.tableXref[s] = {};
	end
	XVar.tableXref[s][#XVar.tableXref[s] + 1] = prefix.."*";
	if (XVar.dumpCache[s]) then
		file:write(XFormat("%s*=%s  <<< see above\n", prefix, s));
	else
		file:write(XFormat("%s*=%s\n", prefix, s));
		XVar.dumpCache[s] = true;
		local keys = {};
		for k,v in pairs(obj) do
			keys[#keys + 1] = k;
		end
		table.sort(keys, function (a,b) return XTableComp(a,keys[a],b,keys[b]); end);
		for idx,k in ipairs(keys) do
			local v = obj[k];
			XDumpObjectToFile(file,prefix..tostring(k), v, depth-1, short, omitFunctions);
			if ((k=="id") and (type(v)=="userdata")) then
				local e = System.GetEntity(v);
				if (e and e~=obj) then
					XDumpObjectToFile(file,"<"..prefix..".id>", e, depth-1, short, omitFunctions);
				end
			end
		end
	end
end

function XTableComp(k1,v1,k2,v2)
	if (tonumber(k1)) then
		if (tonumber(k2)) then
			return tonumber(k1)<tonumber(k2);
		else
			return true; -- numbers before strings
		end
	else
		if (tonumber(k2)) then
			return false; -- numbers before strings
		else
			return tostring(k1)<tostring(k2);
		end
	end
end

function XClampInteger(v, min, max)
	if (v<min) then
		return min;
	end
	if (v>max) then
		return max;
	end
	return v;
end

function XEntityToString(entity)
	if (not entity) then
		return "*:NIL";
	end
	if (type(entity)~="table") then
		return "::"..tostring(entity);
	end
	if (not entity.GetName) then
		return (entity.class or "NIL")..":"..tostring(entity);
	else
		return (entity.class or "NIL")..":"..entity:GetName();
	end
end

function XXSafeGetName(e)
	if (e) then
		if (e.GetName) then
			return e:GetName() or "N/A";
		else
			return tostring(e);
		end
	end
	return "NIL";
end

function XStringSplit(s, sep, max)
	if (not s) then
		return {};
	end
	if (s=="") then
		return {""};
	end
	if (not sep or (sep=="") or (max and (max < 2))) then
		return {s};
	end
	local rv = {};
	local rest = 1;
	for value,pos in string.gmatch(s, "(.-)"..sep.."()") do
		rest = pos;
		table.insert(rv, value);
		if (max and (#rv>=max-1)) then
			break;
		end
	end
	table.insert(rv,string.sub(s,rest));
	return rv;
end

function XArrayEquals(array1, array2)
	if (array1==array2) then
		return true;
	end
	if (#array1 ~= #array2) then
		return false;
	end
	for i,v1 in ipairs(array1) do
		v2 = array2[i];
		if (v1 ~= v2) then
			return false;
		end
	end
	return true;
end

function XArrayToTable(array, key, filter)
	if (not array) then
		return array;
	end
	if (not filter) then
		filter = function(v) return v; end;
	end
	local rv = {};
	if (not key) then
		for i,v in ipairs(array) do
			rv[i] = filter(v);
		end
		return rv;
	end
	local dup = 1;
	for i,v in ipairs(array) do
		if (v[key]) then
			if (not rv[v[key]]) then
				rv[v[key]] = filter(v);
			else
				rv["duplicate_"..dup] = filter(v);
				if (type(rv["duplicate_"..dup])=="table") then
					rv["duplicate_"..dup].duplicate_key = v[key];
				end
				dup = dup + 1;
			end
		else
			if (not rv[i]) then
				rv[i] = filter(v);
			else
				rv["duplicate_"..dup] = filter(v);
				if (type(rv["duplicate_"..dup])=="table") then
					rv["duplicate_"..dup].duplicate_key = i;
				end
				dup = dup + 1;
			end
		end
	end
	return rv;
end

-- a convenience call to XGetPlayerForName with minimal error handling
function XSimplePlayerForName(name, msgtag)
	local player,error,matches = XGetPlayerForName(name);
	if (not player) then
		LogAdmins("[AEGIS] %s: %s", msgtag, error);
		if (matches and #matches>0) then
			LogAdmins("[AEGIS] matches: %s", table.concat(matches," "));
		end
		return nil;
	end
	return player
end

function XGetPlayerForName(name)
	if (not name or name=="" or name=='"' or name=="*" or name=="?" or name=="/" or type(name)~="string") then
		if ((type(name)=="table") and (name.class=="Player")) then
			return name,nil,{name}; -- support direct use with player entity
		end
		return nil, "no valid pattern for player name specified", {};
	end
	local mode = "";
	local results = {};
	local fc = string.sub(name,1,1);
	if (fc=='!') then
		name = string.sub(name,2);
		if (string.find(name,'"',1,true)) then
			-- dont allow " or else this might lead to code injection
			return nil,"use of '\034' in literal pattern is not allowed", {};
		end
		local chunk,error = loadstring('return "'..name..'"');
		if (not chunk) then
			return nil,"loadstring: "..error,{};
		end
		local status,pattern = pcall(chunk);
		if (not status) then
			return nil,"pcall: "..tostring(pattern),{};
		end

		local check1 = string.lower(pattern);
		local check2 = check1;
		if (XCfgVar and XCfgVar.adminTag) then
			check2 = string.lower("["..XCfgVar.adminTag.."]"..pattern);
		end
		local players = System.GetEntitiesByClass("Player");
		for i,player in ipairs(players) do
			local playerName= string.lower(player:GetName());
			if ((check1==playerName) or (check2==playerName)) then
				table.insert(results,player);
			end
		end
		mode=XFormat("literal pattern !%s",pattern);
	elseif (fc=='?') then
		local pattern = string.sub(name,2);
		local regex = string.gsub(string.gsub(string.gsub(pattern,"(%W)","%%%1"),"%%%*","%%?*"),"%%%?",".");

		local players = System.GetEntitiesByClass("Player");
		for i,player in ipairs(players) do
			local playerName= player:GetName();
			if (string.match(playerName,regex)) then
				table.insert(results,player);
			end
		end
		mode=XFormat("simple pattern %q (==>/%s/)",pattern,regex);
	elseif (fc=='/') then
		local pattern,options = string.match(name,"^%/(.+)%/(i?)$");
		if (not pattern) then
			return nil, "invalid regex pattern: "..name,{};
		end
		local status, error = pcall(string.match, "", pattern);
		if (not status) then
			return nil, "error on regex pattern: "..name..": "..tostring(error);
		end

		local players = System.GetEntitiesByClass("Player");
		for i,player in ipairs(players) do
			local playerName= player:GetName();
			if (options=="i") then
				playerName = string.lower(playerName);
			end
			if (string.match(playerName,pattern)) then
				table.insert(results,player);
			end
		end
		mode=XFormat("regex pattern /%s/%s",pattern,options);
	else
		local pattern = name;
		if (fc=='*') then
			pattern = string.sub(name,2);
		end

		local lcpattern = string.lower(pattern);
		local players = System.GetEntitiesByClass("Player");
		--[[
		for i,player in ipairs(players) do
			if (string.find(player:GetName(),pattern,1,true)) then
				table.insert(results,player);
			end
		end
		]]
		if (#results==0) then
			for i,player in ipairs(players) do
				if (string.find(string.lower(player:GetName()),lcpattern,1,true)) then
					table.insert(results,player);
				end
			end
		end
		--[[
		if (#results==0) then
			for i,player in ipairs(players) do
				if (string.find(string.gsub(player:GetName(),"%W",""),pattern,1,true)) then
					table.insert(results,player);
				end
			end
		end
		]]
		if (#results==0) then
			for i,player in ipairs(players) do
				if (string.find(string.lower(string.gsub(player:GetName(),"%W","")),lcpattern,1,true)) then
					table.insert(results,player);
				end
			end
		end
		mode=XFormat("partial pattern *%s",pattern);
	end
	if (#results==0) then
		return nil, "no matches for "..mode, {};
	elseif (#results==1) then
		return results[1],nil,results;
	else
		return nil, "multiple matches for "..mode, results;
	end
end


function XGetPlayerByName(name)
	if (not name or name=="" or type(name)~="string") then
		return nil;
	end
	local players = System.GetEntitiesByClass("Player");
	for i,player in ipairs(players) do
		local playerName = player:GetName();
		if (name==playerName) then
			return player
		end
	end
	return nil;
end

XTableDumper = {
	indent = 0;
	file = nil;
};

function XTableDumper:Write(fmt,...)
	self.file:write(string.rep("\t",self.indent),XFormat(fmt,...),"\n");
end

function XTableDumper:Reset(file)
	self.indent = 0;
	self.file = file or self;
end

function XTableDumper:write(...)
	LogAlways("%s",table.concat({...},""));
end

function XTableDumper:Dump(name,value)
	local t = type(value);
	if (t=="table") then
		local numKeys = 0;
		for k,v in pairs(value) do numKeys=numKeys+1; end;
		if ((numKeys==3) and value.x and value.y and value.z) then
			self:Write("%s = { x=%g, y=%g, z=%g };", name, value.x, value.y, value.z);
		else
			self:Write("%s = {",name);
			if (numKeys == #value) then
				self:DumpArray(value)
			else
				self:DumpTable(value)
			end
			self:Write("};");
		end
	elseif (t=="number") then
		self:Write("%s = %g;",name, value);
	elseif (t=="string") then
		self:Write("%s = %q;",name, value);
	elseif (t=="boolean") then
		if (value) then
			self:Write("%s = true;",name);
		else
			self:Write("%s = false;",name);
		end
	else
		self:Write("%s = nil; -- %s: %s;",name or "???", t or "???", tostring(value) or "???");
	end
end

function XTableDumper:DumpValue(value)
	local t = type(value);
	if (t=="table") then
		local numKeys = 0;
		for k,v in pairs(value) do numKeys=numKeys+1; end;
		if ((numKeys==3) and value.x and value.y and value.z) then
			self:Write("{ x=%g, y=%g, z=%g },", value.x, value.y, value.z);
		else
			self:Write("{");
			if (numKeys == #value) then
				self:DumpArray(value)
			else
				self:DumpTable(value)
			end
			self:Write("},");
		end
	elseif (t=="number") then
		self:Write("%g,", value);
	elseif (t=="string") then
		self:Write("%q,", value);
	elseif (t=="boolean") then
		if (value) then
			self:Write("true,");
		else
			self:Write("false,");
		end
	else
		self:Write("nil, -- %s: %s", t or "???", tostring(value) or "???");
	end
end

function XTableDumper:DumpArray(tbl)
	local keys = {};
	for k,v in pairs(tbl) do
		table.insert(keys, k);
	end
	table.sort(keys, function (a,b) return XTableComp(a,keys[a],b,keys[b]); end);
	self.indent = self.indent + 1;
	for idx,key in ipairs(keys) do
		self:DumpValue(tbl[key]);
	end
	self.indent = self.indent - 1;
end

function XTableDumper:DumpTable(tbl)
	local keys = {};
	for k,v in pairs(tbl) do
		table.insert(keys, k);
	end
	table.sort(keys, function (a,b) return XTableComp(a,keys[a],b,keys[b]); end);
	self.indent = self.indent + 1;
	for idx,key in ipairs(keys) do
		self:Dump(key,tbl[key]);
	end
	self.indent = self.indent - 1;
end

function XCreateRandomString(len)
	local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_";
	local n = string.len(chars);
	local s = "";
	for i = 1,len do
		local k = math.random(n);
		s = s..string.sub(chars,k,k);
	end
	return s;
end

function XTimeStamp(time)
	return os.date("%Y%m%d-%H%M%S",time);
end

function XTimeStampB(time)
	return os.date("%d/%m/%Y - %H:%M:%S",time);
end

function XTimeAgo(seconds)
	local s = math.max(0,math.floor(seconds));
	if (s<60) then
		return XFormat("%4ds",s);
	end
	local m = math.floor(s/60);
	if (m<60) then
		return XFormat("%2d'%02d",m,s-m*60);
	end
	local h = math.floor(m/60);
	if (h<24) then
		return XFormat("%2dh%02d",h,m-60*h);
	end
	-- lets be optimistic regarding uptime :)
	local d = math.floor(h/24);
	if (d<10) then
		return XFormat("%1dd%2dh",d,h-24*d);
	end
	return XFormat("%4dd",d);
end


-- ===============================================
-- anti flooding counters
-- ===============================================

XValve = {
	key = nil;
	parent = nil;
	trunk = true;-- true = dont propagate cooling from parent

	temp = 0;
	clog = 0;
	hot = false;
	
	heatLimit = 60;
	coolLimit = 48;
	heatStep = 12;
	coolStep = -1;
	clogSize = 0;
	
	-- "static" fields
	valves = {};
};

function XValve:forKey(key)
	return self.valves[key] or self:new({key=key});
end

function XValve:new(obj)
	obj = obj or {};
	obj.key = obj.key or obj;
	obj.trunk = false;
	obj.valves = {};
	obj.parent = self;
	setmetatable(obj, self);
	self.__index = self;
	return obj
end

function XValve:__tostring()
	return string.format("XValve(%d/%d)[%s%d:%d]", self.coolLimit, self.heatLimit, ((self.hot and "!") or " "), self.temp, self.clog);
end

function XValve:heat()
	if (self.hot or self.clog>0) then
		return false;
	else
		local t = self.temp + self.heatStep
		self.temp = t;
		self.hot = (t >= self.heatLimit);
		self.clog = self.clogSize;
		if (self.parent) then
			self.parent.valves[self.key] = self;
		end
		return true;
	end
end

function XValve:cool()
	for k,v in pairs(self.valves) do
		if (not v.trunk) then
			v:cool();
		end;
	end
	if (self.clog>0) then
		self.clog = self.clog - 1;
	end
	if (self.temp>0) then
		local t  = self.temp + self.coolStep;
		self.temp = t;
		if (self.hot) then
			self.hot = (t > self.coolLimit);
		end
		return true;
	else
		if (self.parent) then
			self.parent.valves[self.key] = nil;
		end
		return false;
	end
end

XChatValve = XValve:new({clogSize=6;});

XLogValve = XValve:new({clogSize=60;});

AEGIS.Utils = true;